package ca.uvic.cs.cogz.mapping.ui;

import java.awt.BorderLayout;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.swing.Icon;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreePath;

import ca.uvic.cs.chisel.cajun.graph.FlatGraph;
import ca.uvic.cs.chisel.cajun.graph.node.DefaultGraphNodeStyle;
import ca.uvic.cs.chisel.cajun.graph.node.GraphNode;
import ca.uvic.cs.cogz.mapping.SuggestionsManager;
import ca.uvic.cs.cogz.mapping.TemporaryMappingManager;
import ca.uvic.cs.cogz.util.IconConstants;
import ca.uvic.cs.pqviz.common.GraphController;
import edu.stanford.smi.protege.model.Frame;
import edu.stanford.smi.protege.model.KnowledgeBase;
import edu.stanford.smi.protege.util.LabeledComponent;
import edu.stanford.smi.protege.util.LazyTreeNode;
import edu.stanford.smi.protegex.prompt.Mappings;
import edu.stanford.smi.protegex.prompt.actionLists.ActionArgs;
import edu.stanford.smi.protegex.prompt.event.PromptEvent;
import edu.stanford.smi.protegex.prompt.event.PromptListener;
import edu.stanford.smi.protegex.prompt.event.PromptListenerManager;
import edu.stanford.smi.protegex.prompt.operation.MergeClsesOperation;
import edu.stanford.smi.protegex.prompt.operation.Operation;
import edu.stanford.smi.protegex.prompt.ui.TabComponent;
import edu.stanford.smi.protegex.prompt.ui.diffUI.DiffTableView;

/**
 * The neighborhood display tab. This is added to the source and target tab components in Prompt.
 * 
 * @author seanf
 */
public class NeighborhoodPanel extends JPanel {
	private static final int Y_BUFFER = 2;

	private static final long serialVersionUID = 1L;

	/** reference to the QueryView object that displays a node's neighborhood representation */
	//private QueryView neighborhood;
	private GraphController graphController;

	/** the name of the ontology for this display */
	private String ontologyName;

	/** the ontology project reference for the project node's we are visualizing */
	private KnowledgeBase knowledgeBase;

	/** the currently visible Cls */
	private Frame currentClass = null;

	/** reference to our main panel display */
	private LabeledComponent mainPanel;

	/** the JPanel that contains the QueryView */
	private JPanel queryPanel;

	/** reference to the manager containing all the Prompt suggestions */
	private SuggestionsManager suggestionManager;

	/** reference to the manager containing all the temporary mappings */
	private TemporaryMappingManager temporaryMappingManager;

	private Frame selectedCls;

	private JTree tree;

	public NeighborhoodPanel(JTree tree, KnowledgeBase knowledgeBase, SuggestionsManager suggestionManager, TemporaryMappingManager temporaryMappingManager) {
		this.tree = tree;
		this.knowledgeBase = knowledgeBase;
		this.suggestionManager = suggestionManager;
		this.temporaryMappingManager = temporaryMappingManager;
		this.ontologyName = knowledgeBase.getName();

		initScreen();
	}

	private void initScreen() {
		this.setLayout(new BorderLayout());

		queryPanel = new JPanel(new BorderLayout());
		mainPanel = new LabeledComponent(ontologyName, queryPanel, true);

		this.add(mainPanel, BorderLayout.CENTER);

		this.tree.addTreeSelectionListener(new TreeSelectionListener() {
			public void valueChanged(TreeSelectionEvent e) {
				TreePath path = e.getPath();
				if (path != null) {
					LazyTreeNode node = (LazyTreeNode) path.getLastPathComponent();
					if(node.getUserObject() instanceof Frame) {
						Frame selectedCls = (Frame) node.getUserObject();
						showNeighborhood(selectedCls);
					}
				}
			}
		});

		// listen to prompt events
		PromptListenerManager.addListener(new PromptListener() {
			public void operationStarted(Operation operation, PromptEvent event) {}

			public void diffDone(PromptEvent event) {}

			public void diffTableViewBuilt(DiffTableView view, PromptEvent event) {}

			public void diffUIBuilt(JTabbedPane mainPane, PromptEvent event) {}

			public void initializationDone(PromptEvent event) {}

			public void taskComplete(PromptEvent event, boolean interrupted) {}

			public void UIBuilt(TabComponent promptTab, PromptEvent event) {}

			public void operationCompleted(Operation operation, PromptEvent event) {
				// if a merge operation was performed, update our stats
				if (operation instanceof MergeClsesOperation) {
					graphController.refresh();
				}
			}
		});

		// listen to changes on temporary mappings
		temporaryMappingManager.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent e) {
				graphController.refresh();
			}
		});

		showNeighborhood();
	}

	public void showNeighborhood(Frame selectedCls) {
		this.selectedCls = selectedCls;

		// do not display the neighborhood if this tab isn't visible
		if (NeighborhoodPanel.this.isShowing()) {
			showNeighborhood();
		}
	}

	/**
	 * Displays the neighborhood for the given class in the given QueryView object
	 * 
	 * @param selectedCls The focused class we want to start the neighborhood at
	 * @param neighborhood The QueryView object
	 * @param project The Protege project
	 * @param container The container to add the neighborhood viewer to
	 */
	public void showNeighborhood() {
		// for an unsupported type, display the root and update the header label
		if (selectedCls == null) {
			selectedCls = knowledgeBase.getRootCls();
			mainPanel.setHeaderLabel(ontologyName + " - unsupported type");
		} else {
			mainPanel.setHeaderLabel(ontologyName);
		}

		if (selectedCls.equals(currentClass)) {
			return;
		}

		if (graphController == null) {
			graphController = new GraphController(queryPanel, knowledgeBase);
			initNodeIcons();

			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					if (selectedCls.equals(knowledgeBase.getRootCls())) {
						graphController.displayAsSingleNode(selectedCls);
					} else {
						graphController.display(selectedCls);
					}
					Collection<GraphNode> nodes = new ArrayList<GraphNode>();
					nodes.add(graphController.getModel().getNode(selectedCls));
					graphController.getGraph().setMatchingNodes(nodes);
				}
			});

			queryPanel.invalidate();
			queryPanel.validate();
		} else {
			GraphNode node = graphController.getModel().getNode(currentClass);
			if (node != null) {
				node.setSelected(false);
			}

			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					graphController.display(selectedCls);
					Collection<GraphNode> nodes = new ArrayList<GraphNode>();
					GraphNode node = graphController.getModel().getNode(selectedCls);
					if (node != null) {
						nodes.add(node);
						graphController.getGraph().setMatchingNodes(nodes);
					}
				}
			});
		}

		currentClass = selectedCls;
	}

	/**
	 * Sets up the graph node style for the neighborhood visualization. A plus icon is shown for
	 * expandable nodes and a mapping, candidate, or temporary mapping icon is shown on the right
	 * hand side depending on the state of that node's frame object.
	 */
	private void initNodeIcons() {
		// show the plus icon for expandable nodes
		DefaultGraphNodeStyle nodeStyle = new DefaultGraphNodeStyle() {
			public Collection<Icon> getOverlayIcons(GraphNode graphNode) {
				Collection<Icon> icons = new ArrayList<Icon>();
				Frame frame = (Frame) graphNode.getUserObject();
				if (graphController.getModel().isExpandable(graphNode) && !graphController.getModel().isExpanded(graphNode)) {
					icons.add(ca.uvic.cs.pqviz.common.util.IconConstants.ICON_PLUS);
				}
				if (isMapped(frame)) {
					icons.add(IconConstants.ICON_MAPPED);
				} else if (isTemporary(frame)) {
					icons.add(IconConstants.ICON_TEMPORARILY_MAPPED);
				} else if (hasCandidate(frame)) {
					icons.add(IconConstants.ICON_CANDIDATE);
				}

				return icons;
			}

			public Point2D getOverlayIconPosition(GraphNode graphNode, Icon icon) {
				if (icon != null && icon.equals(ca.uvic.cs.pqviz.common.util.IconConstants.ICON_PLUS)) {
					return DefaultGraphNodeStyle.getTopLeft(graphNode.getBounds());
				}
				Point2D topRight = DefaultGraphNodeStyle.getTopRight(graphNode.getBounds());
				if(icon != null) {
					topRight.setLocation(topRight.getX() - icon.getIconWidth(), topRight.getY() + Y_BUFFER);
				}
				else {
					topRight.setLocation(topRight.getX(), topRight.getY() + Y_BUFFER);
				}

				return topRight;
			}
		};
		((FlatGraph) graphController.getGraph()).setGraphNodeStyle(nodeStyle);
	}

	/**
	 * Checks whether the given frame has been mapped.
	 */
	private boolean isMapped(Frame frame) {
		Object targetCls = Mappings.getWhatBecameOfIt(frame);
		if (targetCls != null) {
			return Mappings.getSources((Frame) targetCls) != null;
		}

		return false;
	}

	/**
	 * Checks whether the given frame has any candidate mappings.
	 */
	@SuppressWarnings("unchecked")
	private boolean hasCandidate(Frame frame) {
		List suggestions = suggestionManager.getAllSuggestions();

		for (int i = 0; i < suggestions.size(); i++) {
			Operation operation = (Operation) suggestions.get(i);

			if (operationContainsFrame(operation, frame)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Checks whether the given frame has been temporarily mapped.
	 */
	private boolean isTemporary(Frame frame) {
		for (Operation operation : temporaryMappingManager.getTemporaryMappings()) {
			if (operationContainsFrame(operation, frame)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Checks whether the given operation contains the given frame as an argument.
	 */
	private boolean operationContainsFrame(Operation operation, Frame frame) {
		ActionArgs args = operation.getArgs();
		if (args != null) {
			if (args.getArg(0).equals(frame) || args.getArg(1).equals(frame)) {
				return true;
			}
		}
		return false;
	}
}
